package org.radargun.reporting.html; import java.awt.*; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Type; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; import org.jfree.chart.ChartColor; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartRenderingInfo; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.encoders.EncoderUtil; import org.jfree.chart.encoders.ImageFormat; import org.jfree.chart.plot.IntervalMarker; import org.jfree.chart.plot.ValueMarker; import org.jfree.chart.plot.XYPlot; import org.jfree.data.time.Hour; import org.jfree.data.time.Millisecond; import org.jfree.data.time.Minute; import org.jfree.data.time.RegularTimePeriod; import org.jfree.data.time.Second; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.ui.RectangleAnchor; import org.jfree.ui.RectangleInsets; import org.jfree.ui.TextAnchor; import org.radargun.config.Converter; import org.radargun.logging.Log; import org.radargun.logging.LogFactory; import org.radargun.reporting.Timeline; /** * Chart showing the events, intervals and values from {@link Timeline} * * @author Radim Vansa <rvansa@redhat.com> */ public class TimelineChart { private static final Log log = LogFactory.getLog(TimelineChart.class); private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); private static final Paint[] DEFAULT_PAINTS = ChartColor.createDefaultPaintArray(); private static final int LABEL_OFFSET = 15; private static final int DOMAIN_OFFSET = 3; private static final int MAX_EVENT_VALUES = 1000; private int width = 1024; private int height = 768; private Class<? extends RegularTimePeriod> timePeriodClass = Second.class; private Paint paint; //private Shape shape; private Stroke stroke = new BasicStroke(1); private long startTimestamp; private long endTimestamp; private JFreeChart chart; public TimelineChart() { this(Color.RED); } public TimelineChart(Color paint) { this.paint = paint; } public void setEvents(List<? extends Object> events, int slaveIndex, long startTimestamp, long endTimestamp, double lowerBound, double upperBound) { int paintIndex = slaveIndex % DEFAULT_PAINTS.length; if (paintIndex < 0) paintIndex += DEFAULT_PAINTS.length; paint = DEFAULT_PAINTS[paintIndex]; this.startTimestamp = startTimestamp; this.endTimestamp = endTimestamp + (startTimestamp == endTimestamp ? 1 : 0); TimeSeries series = new TimeSeries("Slave " + slaveIndex); TimeSeriesCollection dataset = new TimeSeriesCollection(series, GMT); chart = ChartFactory.createTimeSeriesChart(null, "Time from start", null, dataset, false, false, false); chart.setBackgroundPaint(new Color(0, 0, 0, 0)); XYPlot plot = chart.getXYPlot(); plot.getRenderer().setSeriesPaint(0, paint); plot.setBackgroundAlpha(0); plot.setDomainGridlinesVisible(false); plot.setDomainZeroBaselineVisible(true); plot.setRangeGridlinesVisible(false); plot.setRangeZeroBaselineVisible(true); Number[] minValues = new Number[MAX_EVENT_VALUES]; Number[] maxValues = new Number[MAX_EVENT_VALUES]; long[] minTimestamps = new long[MAX_EVENT_VALUES]; long[] maxTimestamps = new long[MAX_EVENT_VALUES]; for (Object event : events) { if (event instanceof Timeline.Value) { Timeline.Value value = (Timeline.Value) event; if (value.timestamp > this.endTimestamp) { throw new IllegalStateException(String.format("Current timestamp %d is bigger then end timestamp %d", value.timestamp, this.endTimestamp)); } int bucket = (int) ((value.timestamp - startTimestamp) * (MAX_EVENT_VALUES-1) / (this.endTimestamp - startTimestamp)); if (minValues[bucket] == null) { minValues[bucket] = value.value; maxValues[bucket] = value.value; minTimestamps[bucket] = value.timestamp; maxTimestamps[bucket] = value.timestamp; } else { if (minValues[bucket].doubleValue() > value.value.doubleValue()) { minValues[bucket] = value.value; } if (maxValues[bucket].doubleValue() < value.value.doubleValue()) { maxValues[bucket] = value.value; } minTimestamps[bucket] = Math.min(minTimestamps[bucket], value.timestamp); maxTimestamps[bucket] = Math.max(maxTimestamps[bucket], value.timestamp); } } else if (event instanceof Timeline.IntervalEvent) { Timeline.IntervalEvent intervalEvent = (Timeline.IntervalEvent) event; IntervalMarker marker = new IntervalMarker(intervalEvent.timestamp - startTimestamp, intervalEvent.timestamp + intervalEvent.duration - startTimestamp, paint, stroke, paint, stroke, 0.3f); marker.setLabel(intervalEvent.description); marker.setLabelAnchor(RectangleAnchor.BOTTOM); marker.setLabelTextAnchor(TextAnchor.BOTTOM_CENTER); marker.setLabelOffset(new RectangleInsets(0, 0, (slaveIndex + 1) * LABEL_OFFSET, 0)); plot.addDomainMarker(marker); } else if (event instanceof Timeline.TextEvent) { Timeline.TextEvent textEvent = (Timeline.TextEvent) event; ValueMarker marker = new ValueMarker(textEvent.timestamp - startTimestamp, paint, stroke); marker.setLabel(textEvent.text); marker.setLabelAnchor(RectangleAnchor.BOTTOM_LEFT); marker.setLabelTextAnchor(TextAnchor.BOTTOM_LEFT); marker.setLabelOffset(new RectangleInsets(0, 0, (slaveIndex + 1) * LABEL_OFFSET, 0)); plot.addDomainMarker(marker); } } for (int bucket = 0; bucket < MAX_EVENT_VALUES; ++bucket) { if (minValues[bucket] == null) continue; series.addOrUpdate(time(minTimestamps[bucket] - startTimestamp), minValues[bucket]); series.addOrUpdate(time(maxTimestamps[bucket] - startTimestamp), maxValues[bucket]); } DateAxis dateAxis = (DateAxis) plot.getDomainAxis(); dateAxis.setTimeZone(GMT); dateAxis.setMinimumDate(new Date(0)); dateAxis.setMaximumDate(new Date(endTimestamp - startTimestamp)); if (upperBound > lowerBound) { plot.getRangeAxis().setRange(lowerBound, upperBound); } } public void saveChart(String filename) throws IOException { try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filename))) { chart.getXYPlot().getRangeAxis().setVisible(false); chart.getXYPlot().getDomainAxis().setVisible(false); ChartRenderingInfo renderingInfo = new ChartRenderingInfo(); BufferedImage image = chart.createBufferedImage(width, height, renderingInfo); EncoderUtil.writeBufferedImage(image, ImageFormat.PNG, out); } } public void saveRange(String filename) throws IOException { try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filename))) { chart.getXYPlot().getRangeAxis().setVisible(true); chart.getXYPlot().getDomainAxis().setVisible(false); ChartRenderingInfo renderingInfo = new ChartRenderingInfo(); BufferedImage image = chart.createBufferedImage(width, height, renderingInfo); image = image.getSubimage(0, 0, (int) renderingInfo.getPlotInfo().getDataArea().getX(), height); EncoderUtil.writeBufferedImage(image, ImageFormat.PNG, out); } } public void saveRelativeDomain(String filename) throws IOException { try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filename))) { chart.getXYPlot().getRangeAxis().setVisible(false); chart.getXYPlot().getDomainAxis().setVisible(true); ChartRenderingInfo renderingInfo = new ChartRenderingInfo(); BufferedImage image = chart.createBufferedImage(width, height, renderingInfo); int maxY = (int) renderingInfo.getPlotInfo().getDataArea().getMaxY(); image = image.getSubimage(0, maxY + DOMAIN_OFFSET, width, height - maxY - DOMAIN_OFFSET); EncoderUtil.writeBufferedImage(image, ImageFormat.PNG, out); } } public void saveAbsoluteDomain(String filename) throws IOException { try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filename))) { chart.getXYPlot().getRangeAxis().setVisible(false); Font labelFont = chart.getXYPlot().getDomainAxis().getLabelFont(); chart.getXYPlot().setDomainAxis(new DateAxis("Time")); chart.getXYPlot().getDomainAxis().setRange(startTimestamp, endTimestamp); chart.getXYPlot().getDomainAxis().setLabelFont(labelFont); ChartRenderingInfo renderingInfo = new ChartRenderingInfo(); BufferedImage image = chart.createBufferedImage(width, height, renderingInfo); int maxY = (int) renderingInfo.getPlotInfo().getDataArea().getMaxY(); image = image.getSubimage(0, maxY + DOMAIN_OFFSET, width, height - maxY - DOMAIN_OFFSET); EncoderUtil.writeBufferedImage(image, ImageFormat.PNG, out); } } private RegularTimePeriod time(long timestamp) { Date date = new Date(timestamp); return RegularTimePeriod.createInstance(timePeriodClass, date, GMT); } public static int getColorForIndex(int slaveIndex) { if (slaveIndex < 0) return 0; return ((Color) DEFAULT_PAINTS[slaveIndex % DEFAULT_PAINTS.length]).getRGB() & 0xFFFFFF; } public void setDimensions(int width, int height) { this.width = width; this.height = height; } private static class TimeUnitConverter implements Converter<Class<? extends RegularTimePeriod>> { @Override public Class<? extends RegularTimePeriod> convert(String string, Type type) { if (string == null) throw new NullPointerException(); string = string.toLowerCase(Locale.ENGLISH); if ("millisecond".equals(string)) { return Millisecond.class; } if ("second".equals(string)) { return Second.class; } if ("minute".equals(string)) { return Minute.class; } if ("hour".equals(string)) { return Hour.class; } throw new IllegalArgumentException(string); } @Override public String convertToString(Class<? extends RegularTimePeriod> value) { return value.getSimpleName().toLowerCase(Locale.ENGLISH); } @Override public String allowedPattern(Type type) { return "millisecond|second|minute|hour"; } } }